Skip to content

Implement server auth bootstrap and pairing flow#1768

Open
juliusmarminge wants to merge 49 commits intomainfrom
t3code/remote-auth-pairing
Open

Implement server auth bootstrap and pairing flow#1768
juliusmarminge wants to merge 49 commits intomainfrom
t3code/remote-auth-pairing

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Apr 6, 2026

Summary

  • add a unified server auth model with bootstrap credentials, signed sessions, and HTTP/WS protection
  • remove the legacy static auth token path and add a dedicated /pair route for browser pairing
  • move auth gating into router beforeLoad, add a first-paint loading shell, and document the auth architecture

Testing

  • bun fmt
  • bun lint
  • bun typecheck
  • cd apps/web && bun run test src/authBootstrap.test.ts
  • cd apps/server && bun run test src/server.test.ts -t "rejects reusing the same bootstrap credential after it has been exchanged"

Note

High Risk
Introduces a new server-wide authentication system (bootstrap credentials, signed sessions, WS tokens) and removes the legacy static token path, impacting all HTTP/WS access and potentially breaking existing clients/configs that relied on authToken/T3CODE_AUTH_TOKEN.

Overview
Adds a new server-wide auth subsystem built around one-time bootstrap credentials, signed session tokens (cookie + bearer), and short-lived WebSocket upgrade tokens, exposed via new /api/auth/* HTTP routes for session state, bootstrap exchange, pairing token management, and client/session revocation.

Updates server routing to enforce authentication on privileged HTTP surfaces (e.g. attachments/observability proxy) using ServerAuth, and introduces policy-driven defaults (desktop-managed-local, loopback-browser, remote-reachable) based on mode/host binding.

Extends the desktop app to support configurable server exposure (local-only vs network-accessible) persisted to disk with IPC APIs, switches startup to a desktop bootstrap token (replacing the old authToken flow), and adds backend readiness polling for dev/prod startup sequencing.

Also adds architecture/planning docs for remote environments and the long-term auth model, plus minor tooling config updates (formatter ignore patterns).

Reviewed by Cursor Bugbot for commit 853d447. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Implement server auth bootstrap and pairing flow with session management

  • Adds a full server-side auth system: pairing link issuance (one-time tokens), bearer/cookie session tokens, WebSocket token minting, and a /pair route in the web app for credential exchange
  • New CLI subcommands (auth pairing create/list/revoke, auth session issue/list) manage credentials from the command line; desktop mode passes a desktopBootstrapToken instead of the old authToken
  • Server routes (/api/auth/session, /api/auth/bootstrap, /api/auth/ws-token, etc.) are added and all existing HTTP/WS routes now require authentication via ServerAuth
  • Desktop app gains network-exposure controls (local-only vs. network-accessible), LAN host advertisement, backend readiness polling, and IPC APIs to toggle exposure mode with app relaunch
  • Sidebar and ChatView now group projects across environments, show remote environment badges/labels, and support switching environments from the toolbar
  • A new Connections settings panel lets users manage pairing links (with QR codes), view/revoke client sessions, and add/remove remote environments
  • Risk: authToken is removed from config and CLI; callers must migrate to desktopBootstrapToken. All HTTP and WebSocket endpoints now return 401/403 when unauthenticated, which is a breaking behavioral change for unauthenticated clients.

Macroscope summarized 853d447.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f179bcb3-a4df-40cc-9cbd-ee4a1dd9249c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/remote-auth-pairing

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Apr 6, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Pairing URL points to root, token lost during redirect
    • Added url.pathname = "/pair" in issueStartupPairingUrl so the generated URL navigates directly to /pair?token=..., avoiding the redirect that strips the query parameter.
  • ✅ Fixed: Secret store set swallows write errors silently
    • Changed Effect.map(() => new SecretStoreError(...)) to Effect.flatMap(() => Effect.fail(new SecretStoreError(...))) so write failures properly propagate as Effect errors instead of being swallowed as success values.
  • ✅ Fixed: Bootstrap credential consume has TOCTOU race condition
    • Replaced the non-atomic Ref.get + Ref.update sequence with a single Ref.modify call that atomically reads the grant, validates it, and updates the map in one operation.
  • ✅ Fixed: Duplicate one-time tokens issued during startup
    • In non-desktop mode, resolveStartupBrowserTarget is now called once and the resulting URL is reused for both logging and browser opening, avoiding issuing two separate one-time tokens.

Create PR

Or push these changes by commenting:

@cursor push f99e419f7c
Preview (f99e419f7c)
diff --git a/apps/server/src/auth/Layers/BootstrapCredentialService.ts b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
--- a/apps/server/src/auth/Layers/BootstrapCredentialService.ts
+++ b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
@@ -49,47 +49,60 @@
       return credential;
     });
 
+  type ConsumeResult =
+    | { readonly _tag: "error"; readonly error: BootstrapCredentialError }
+    | { readonly _tag: "ok"; readonly grant: BootstrapGrant };
+
   const consume: BootstrapCredentialServiceShape["consume"] = (credential) =>
     Effect.gen(function* () {
-      const current = yield* Ref.get(grantsRef);
-      const grant = current.get(credential);
-      if (!grant) {
-        return yield* new BootstrapCredentialError({
-          message: "Unknown bootstrap credential.",
-        });
-      }
+      const now = yield* DateTime.now;
+      const result = yield* Ref.modify(
+        grantsRef,
+        (current): readonly [ConsumeResult, Map<string, StoredBootstrapGrant>] => {
+          const grant = current.get(credential);
+          if (!grant) {
+            return [
+              {
+                _tag: "error",
+                error: new BootstrapCredentialError({ message: "Unknown bootstrap credential." }),
+              },
+              current,
+            ];
+          }
 
-      if (DateTime.isGreaterThanOrEqualTo(yield* DateTime.now, grant.expiresAt)) {
-        yield* Ref.update(grantsRef, (state) => {
-          const next = new Map(state);
-          next.delete(credential);
-          return next;
-        });
-        return yield* new BootstrapCredentialError({
-          message: "Bootstrap credential expired.",
-        });
-      }
-
-      const remainingUses = grant.remainingUses;
-      if (typeof remainingUses === "number") {
-        yield* Ref.update(grantsRef, (state) => {
-          const next = new Map(state);
-          if (remainingUses <= 1) {
+          if (DateTime.isGreaterThanOrEqualTo(now, grant.expiresAt)) {
+            const next = new Map(current);
             next.delete(credential);
-          } else {
-            next.set(credential, {
-              ...grant,
-              remainingUses: remainingUses - 1,
-            });
+            return [
+              {
+                _tag: "error",
+                error: new BootstrapCredentialError({ message: "Bootstrap credential expired." }),
+              },
+              next,
+            ];
           }
-          return next;
-        });
+
+          const next = new Map(current);
+          const remainingUses = grant.remainingUses;
+          if (typeof remainingUses === "number") {
+            if (remainingUses <= 1) {
+              next.delete(credential);
+            } else {
+              next.set(credential, { ...grant, remainingUses: remainingUses - 1 });
+            }
+          }
+
+          return [
+            { _tag: "ok", grant: { method: grant.method, expiresAt: grant.expiresAt } },
+            next,
+          ];
+        },
+      );
+
+      if (result._tag === "error") {
+        return yield* result.error;
       }
-
-      return {
-        method: grant.method,
-        expiresAt: grant.expiresAt,
-      } satisfies BootstrapGrant;
+      return result.grant;
     });
 
   return {

diff --git a/apps/server/src/auth/Layers/ServerAuth.ts b/apps/server/src/auth/Layers/ServerAuth.ts
--- a/apps/server/src/auth/Layers/ServerAuth.ts
+++ b/apps/server/src/auth/Layers/ServerAuth.ts
@@ -124,6 +124,7 @@
     bootstrapCredentials.issueOneTimeToken().pipe(
       Effect.map((credential) => {
         const url = new URL(baseUrl);
+        url.pathname = "/pair";
         url.searchParams.set("token", credential);
         return url.toString();
       }),

diff --git a/apps/server/src/auth/Layers/ServerSecretStore.ts b/apps/server/src/auth/Layers/ServerSecretStore.ts
--- a/apps/server/src/auth/Layers/ServerSecretStore.ts
+++ b/apps/server/src/auth/Layers/ServerSecretStore.ts
@@ -47,8 +47,10 @@
       Effect.catch((cause) =>
         fileSystem.remove(tempPath).pipe(
           Effect.orElseSucceed(() => undefined),
-          Effect.map(
-            () => new SecretStoreError({ message: `Failed to persist secret ${name}.`, cause }),
+          Effect.flatMap(() =>
+            Effect.fail(
+              new SecretStoreError({ message: `Failed to persist secret ${name}.`, cause }),
+            ),
           ),
         ),
       ),

diff --git a/apps/server/src/serverRuntimeStartup.ts b/apps/server/src/serverRuntimeStartup.ts
--- a/apps/server/src/serverRuntimeStartup.ts
+++ b/apps/server/src/serverRuntimeStartup.ts
@@ -388,8 +388,22 @@
         yield* Effect.logInfo("Authentication required. Open T3 Code using the pairing URL.", {
           pairingUrl,
         });
+        if (!serverConfig.noBrowser) {
+          const { openBrowser } = yield* Open;
+          yield* runStartupPhase(
+            "browser.open",
+            openBrowser(pairingUrl).pipe(
+              Effect.catch(() =>
+                Effect.logInfo("browser auto-open unavailable", {
+                  hint: `Open ${pairingUrl} in your browser.`,
+                }),
+              ),
+            ),
+          );
+        }
+      } else {
+        yield* runStartupPhase("browser.open", maybeOpenBrowser);
       }
-      yield* runStartupPhase("browser.open", maybeOpenBrowser);
       yield* Effect.logDebug("startup phase: complete");
     }),
   );

You can send follow-ups to the cloud agent here.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 6, 2026

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Auth bootstrap fetch uses unsupported ws:// protocol URL
    • Added a wsUrlToHttpUrl() helper that converts ws:/wss: protocols to http:/https: and applied it at all three call sites where resolvePrimaryEnvironmentBootstrapUrl() is passed to fetch().

Create PR

Or push these changes by commenting:

@cursor push 59ffed3254
Preview (59ffed3254)
diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts
--- a/apps/web/src/authBootstrap.test.ts
+++ b/apps/web/src/authBootstrap.test.ts
@@ -86,10 +86,10 @@
 
     expect(fetchMock).toHaveBeenCalledTimes(2);
     expect(fetchMock.mock.calls[0]?.[0]).toEqual(
-      new URL("/api/auth/session", "ws://localhost:3773/"),
+      new URL("/api/auth/session", "http://localhost:3773/"),
     );
     expect(fetchMock.mock.calls[1]?.[0]).toEqual(
-      new URL("/api/auth/bootstrap", "ws://localhost:3773/"),
+      new URL("/api/auth/bootstrap", "http://localhost:3773/"),
     );
   });
 

diff --git a/apps/web/src/authBootstrap.ts b/apps/web/src/authBootstrap.ts
--- a/apps/web/src/authBootstrap.ts
+++ b/apps/web/src/authBootstrap.ts
@@ -2,6 +2,13 @@
 
 import { resolvePrimaryEnvironmentBootstrapUrl } from "./environmentBootstrap";
 
+function wsUrlToHttpUrl(url: string): string {
+  const parsed = new URL(url);
+  if (parsed.protocol === "ws:") parsed.protocol = "http:";
+  else if (parsed.protocol === "wss:") parsed.protocol = "https:";
+  return parsed.href;
+}
+
 export type ServerAuthGateState =
   | { status: "authenticated" }
   | {
@@ -80,7 +87,7 @@
 }
 
 async function bootstrapServerAuth(): Promise<ServerAuthGateState> {
-  const baseUrl = resolvePrimaryEnvironmentBootstrapUrl();
+  const baseUrl = wsUrlToHttpUrl(resolvePrimaryEnvironmentBootstrapUrl());
   const bootstrapCredential = getBootstrapCredential();
   const currentSession = await fetchSessionState(baseUrl);
   if (currentSession.authenticated) {
@@ -112,7 +119,10 @@
     throw new Error("Enter a pairing token to continue.");
   }
 
-  await exchangeBootstrapCredential(resolvePrimaryEnvironmentBootstrapUrl(), trimmedCredential);
+  await exchangeBootstrapCredential(
+    wsUrlToHttpUrl(resolvePrimaryEnvironmentBootstrapUrl()),
+    trimmedCredential,
+  );
   stripPairingTokenFromUrl();
 }

You can send follow-ups to the cloud agent here.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Cached auth state not invalidated after successful pairing
    • Added bootstrapPromise = null after the successful credential exchange in submitServerAuthCredential, so subsequent calls to resolveInitialServerAuthGateState re-evaluate the auth state instead of returning the stale cached requires-auth promise.

Create PR

Or push these changes by commenting:

@cursor push 264a9be49a
Preview (264a9be49a)
diff --git a/apps/web/src/authBootstrap.ts b/apps/web/src/authBootstrap.ts
--- a/apps/web/src/authBootstrap.ts
+++ b/apps/web/src/authBootstrap.ts
@@ -123,6 +123,7 @@
 
   await exchangeBootstrapCredential(resolvePrimaryEnvironmentHttpBaseUrl(), trimmedCredential);
   stripPairingTokenFromUrl();
+  bootstrapPromise = null;
 }
 
 export function resolveInitialServerAuthGateState(): Promise<ServerAuthGateState> {

You can send follow-ups to the cloud agent here.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 5 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for 2 of the 3 issues found in the latest run.

  • ✅ Fixed: Token split allows extra segments to pass verification
    • Added a check that token.split(".") produces exactly 2 parts before destructuring, rejecting tokens with extra segments.
  • ✅ Fixed: Secret store getOrCreateRandom has TOCTOU race condition
    • Wrapped the read-then-write sequence in getOrCreateRandom with a Semaphore(1) mutex to ensure atomicity.

Create PR

Or push these changes by commenting:

@cursor push f69311f5db
Preview (f69311f5db)
diff --git a/apps/server/src/auth/Layers/ServerSecretStore.ts b/apps/server/src/auth/Layers/ServerSecretStore.ts
--- a/apps/server/src/auth/Layers/ServerSecretStore.ts
+++ b/apps/server/src/auth/Layers/ServerSecretStore.ts
@@ -1,6 +1,6 @@
 import * as Crypto from "node:crypto";
 
-import { Effect, FileSystem, Layer, Path } from "effect";
+import { Effect, FileSystem, Layer, Path, Semaphore } from "effect";
 import * as PlatformError from "effect/PlatformError";
 
 import { ServerConfig } from "../../config.ts";
@@ -60,6 +60,8 @@
     );
   };
 
+  const mutex = yield* Semaphore.make(1);
+
   const getOrCreateRandom: ServerSecretStoreShape["getOrCreateRandom"] = (name, bytes) =>
     get(name).pipe(
       Effect.flatMap((existing) => {
@@ -70,6 +72,7 @@
         const generated = Crypto.randomBytes(bytes);
         return set(name, generated).pipe(Effect.as(Uint8Array.from(generated)));
       }),
+      mutex.withPermits(1),
     );
 
   const remove: ServerSecretStoreShape["remove"] = (name) =>

diff --git a/apps/server/src/auth/Layers/SessionCredentialService.ts b/apps/server/src/auth/Layers/SessionCredentialService.ts
--- a/apps/server/src/auth/Layers/SessionCredentialService.ts
+++ b/apps/server/src/auth/Layers/SessionCredentialService.ts
@@ -56,7 +56,13 @@
   });
 
   const verify: SessionCredentialServiceShape["verify"] = Effect.fn("verify")(function* (token) {
-    const [encodedPayload, signature] = token.split(".");
+    const parts = token.split(".");
+    if (parts.length !== 2) {
+      return yield* new SessionCredentialError({
+        message: "Malformed session token.",
+      });
+    }
+    const [encodedPayload, signature] = parts;
     if (!encodedPayload || !signature) {
       return yield* new SessionCredentialError({
         message: "Malformed session token.",

You can send follow-ups to the cloud agent here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low

function startApp() {

VITE_DEV_SERVER_URL is trimmed and validated by the parent on lines 8-17, but the child process receives the untrimmed value directly from process.env via childEnv. When the original environment variable contains whitespace, the parent accepts the URL after trimming while the child receives the raw value, causing URL parsing failures in the Electron app. Consider passing the validated devServerUrl to the child explicitly, or ensuring childEnv uses the trimmed value.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/desktop/scripts/dev-electron.mjs around line 65:

`VITE_DEV_SERVER_URL` is trimmed and validated by the parent on lines 8-17, but the child process receives the untrimmed value directly from `process.env` via `childEnv`. When the original environment variable contains whitespace, the parent accepts the URL after trimming while the child receives the raw value, causing URL parsing failures in the Electron app. Consider passing the validated `devServerUrl` to the child explicitly, or ensuring `childEnv` uses the trimmed value.

Evidence trail:
apps/desktop/scripts/dev-electron.mjs lines 8, 37, and 65-73 at REVIEWED_COMMIT:
- Line 8: `const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();` (trimmed for validation)
- Line 37: `const childEnv = { ...process.env };` (spreads original untrimmed env)
- Lines 65-73: `spawn(..., { env: childEnv, ... })` (child receives untrimmed value)

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 7 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Session token exposed in response body alongside HttpOnly cookie
    • Stripped sessionToken from the bootstrap JSON response body by destructuring it out before serialization, so the token is only transmitted via the httpOnly cookie.
  • ✅ Fixed: Session cookie missing secure flag for non-loopback environments
    • Added conditional secure: descriptor.policy === "remote-reachable" to the cookie options so the flag is set when the server is configured for remote access.

Create PR

Or push these changes by commenting:

@cursor push 21a3606f32
Preview (21a3606f32)
diff --git a/apps/server/src/auth/http.ts b/apps/server/src/auth/http.ts
--- a/apps/server/src/auth/http.ts
+++ b/apps/server/src/auth/http.ts
@@ -40,12 +40,14 @@
     );
     const result = yield* serverAuth.exchangeBootstrapCredential(payload.credential);
 
-    return yield* HttpServerResponse.jsonUnsafe(result, { status: 200 }).pipe(
+    const { sessionToken: _token, ...responseBody } = result;
+    return yield* HttpServerResponse.jsonUnsafe(responseBody, { status: 200 }).pipe(
       HttpServerResponse.setCookie(descriptor.sessionCookieName, result.sessionToken, {
         expires: DateTime.toDate(result.expiresAt),
         httpOnly: true,
         path: "/",
         sameSite: "lax",
+        secure: descriptor.policy === "remote-reachable",
       }),
     );
   }).pipe(Effect.catchTag("AuthError", (error) => Effect.succeed(toUnauthorizedResponse(error)))),

diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts
--- a/apps/server/src/server.test.ts
+++ b/apps/server/src/server.test.ts
@@ -492,6 +492,12 @@
     return `http://127.0.0.1:${address.port}${pathname}`;
   });
 
+function parseSessionTokenFromSetCookie(setCookie: string | null): string | null {
+  if (!setCookie) return null;
+  const match = /t3_session=([^;]+)/.exec(setCookie);
+  return match?.[1] ?? null;
+}
+
 const bootstrapBrowserSession = (credential = defaultDesktopBootstrapToken) =>
   Effect.gen(function* () {
     const bootstrapUrl = yield* getHttpServerUrl("/api/auth/bootstrap");
@@ -509,13 +515,15 @@
     const body = (yield* Effect.promise(() => response.json())) as {
       readonly authenticated: boolean;
       readonly sessionMethod: string;
-      readonly sessionToken: string;
       readonly expiresAt: string;
     };
+    const cookie = response.headers.get("set-cookie");
+    const sessionToken = parseSessionTokenFromSetCookie(cookie);
     return {
       response,
       body,
-      cookie: response.headers.get("set-cookie"),
+      cookie,
+      sessionToken,
     };
   });
 
@@ -525,18 +533,18 @@
       return cachedDefaultSessionToken;
     }
 
-    const { response, body } = yield* bootstrapBrowserSession(credential);
-    if (!response.ok) {
+    const { response, sessionToken } = yield* bootstrapBrowserSession(credential);
+    if (!response.ok || !sessionToken) {
       return yield* Effect.fail(
         new Error(`Expected bootstrap session response to succeed, got ${response.status}`),
       );
     }
 
     if (credential === defaultDesktopBootstrapToken) {
-      cachedDefaultSessionToken = body.sessionToken;
+      cachedDefaultSessionToken = sessionToken;
     }
 
-    return body.sessionToken;
+    return sessionToken;
   });
 
 const getWsServerUrl = (
@@ -720,13 +728,15 @@
     Effect.gen(function* () {
       yield* buildAppUnderTest();
 
-      const { response: bootstrapResponse, body: bootstrapBody } = yield* bootstrapBrowserSession();
+      const { response: bootstrapResponse, sessionToken: bootstrapSessionToken } =
+        yield* bootstrapBrowserSession();
 
       assert.equal(bootstrapResponse.status, 200);
+      assert(bootstrapSessionToken, "Expected session token in Set-Cookie header");
 
       const wsUrl = appendSessionTokenToUrl(
         yield* getWsServerUrl("/ws", { authenticated: false }),
-        bootstrapBody.sessionToken,
+        bootstrapSessionToken,
       );
       const response = yield* Effect.scoped(
         withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverGetConfig]({})),

diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts
--- a/apps/web/src/authBootstrap.test.ts
+++ b/apps/web/src/authBootstrap.test.ts
@@ -65,7 +65,6 @@
         jsonResponse({
           authenticated: true,
           sessionMethod: "browser-session-cookie",
-          sessionToken: "session-token",
           expiresAt: "2026-04-05T00:00:00.000Z",
         }),
       );
@@ -207,7 +206,6 @@
         jsonResponse({
           authenticated: true,
           sessionMethod: "browser-session-cookie",
-          sessionToken: "session-token",
           expiresAt: "2026-04-05T00:00:00.000Z",
         }),
       );

diff --git a/apps/web/src/authBootstrap.ts b/apps/web/src/authBootstrap.ts
--- a/apps/web/src/authBootstrap.ts
+++ b/apps/web/src/authBootstrap.ts
@@ -1,4 +1,4 @@
-import type { AuthBootstrapInput, AuthBootstrapResult, AuthSessionState } from "@t3tools/contracts";
+import type { AuthBootstrapInput, AuthSessionState } from "@t3tools/contracts";
 import { resolveServerHttpUrl } from "./lib/utils";
 
 export type ServerAuthGateState =
@@ -56,7 +56,7 @@
   return (await response.json()) as AuthSessionState;
 }
 
-async function exchangeBootstrapCredential(credential: string): Promise<AuthBootstrapResult> {
+async function exchangeBootstrapCredential(credential: string): Promise<void> {
   const payload: AuthBootstrapInput = { credential };
   const response = await fetch(resolveServerHttpUrl({ pathname: "/api/auth/bootstrap" }), {
     body: JSON.stringify(payload),
@@ -71,8 +71,6 @@
     const message = await response.text();
     throw new Error(message || `Failed to bootstrap auth session (${response.status}).`);
   }
-
-  return (await response.json()) as AuthBootstrapResult;
 }
 
 async function bootstrapServerAuth(): Promise<ServerAuthGateState> {

diff --git a/apps/web/test/authHttpHandlers.ts b/apps/web/test/authHttpHandlers.ts
--- a/apps/web/test/authHttpHandlers.ts
+++ b/apps/web/test/authHttpHandlers.ts
@@ -2,7 +2,6 @@
 import { HttpResponse, http } from "msw";
 
 const TEST_SESSION_EXPIRES_AT = "2026-05-01T12:00:00.000Z";
-const TEST_SESSION_TOKEN = "browser-test-session-token";
 
 export function createAuthenticatedSessionHandlers(getAuthDescriptor: () => ServerAuthDescriptor) {
   return [
@@ -18,7 +17,6 @@
       HttpResponse.json({
         authenticated: true,
         sessionMethod: "browser-session-cookie",
-        sessionToken: TEST_SESSION_TOKEN,
         expiresAt: TEST_SESSION_EXPIRES_AT,
       }),
     ),

You can send follow-ups to the cloud agent here.

httpOnly: true,
path: "/",
sameSite: "lax",
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session cookie missing secure flag for non-loopback environments

Medium Severity

The session cookie set by the bootstrap endpoint omits the secure flag entirely. While this is fine for local development over HTTP, the auth model explicitly supports remote-reachable environments where TLS is expected. Without secure, the cookie could be sent over plaintext HTTP on a remote/tunneled connection, exposing the session token to network eavesdropping. The flag could be set conditionally based on the auth policy or the request protocol.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dca54c7. Configure here.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 10 total unresolved issues (including 7 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Desktop bootstrap token expires before backend ready
    • Increased DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES from 5 to 10 minutes to provide a safer margin for slow backend startups, window creation, and renderer bootstrap exchange.
  • ✅ Fixed: Unused parameter in buildReconnectTitle after refactoring
    • Removed the dead buildReconnectTitle function entirely and inlined the constant string "Disconnected from T3 Server" at the single call site.
  • ✅ Fixed: Module-level shared mutable state across parallel test runs
    • Replaced the bare cached token string with a generation-tagged object so that stale tokens from prior server builds are automatically invalidated when buildAppUnderTest increments the generation counter.

Create PR

Or push these changes by commenting:

@cursor push d5344ec03b
Preview (d5344ec03b)
diff --git a/apps/server/src/auth/Layers/BootstrapCredentialService.ts b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
--- a/apps/server/src/auth/Layers/BootstrapCredentialService.ts
+++ b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
@@ -22,7 +22,7 @@
       readonly grant: BootstrapGrant;
     };
 
-const DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES = Duration.minutes(5);
+const DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES = Duration.minutes(10);
 
 export const makeBootstrapCredentialService = Effect.gen(function* () {
   const config = yield* ServerConfig;

diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts
--- a/apps/server/src/server.test.ts
+++ b/apps/server/src/server.test.ts
@@ -108,7 +108,8 @@
     repositoryIdentity: true,
   },
 };
-let cachedDefaultSessionToken: string | null = null;
+let serverBuildGeneration = 0;
+let cachedDefaultSessionToken: { token: string; generation: number } | null = null;
 
 const makeDefaultOrchestrationReadModel = () => {
   const now = new Date().toISOString();
@@ -293,7 +294,7 @@
   };
 }) =>
   Effect.gen(function* () {
-    cachedDefaultSessionToken = null;
+    serverBuildGeneration += 1;
     const fileSystem = yield* FileSystem.FileSystem;
     const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" });
     const baseDir = options?.config?.baseDir ?? tempBaseDir;
@@ -521,8 +522,13 @@
 
 const getAuthenticatedSessionToken = (credential = defaultDesktopBootstrapToken) =>
   Effect.gen(function* () {
-    if (credential === defaultDesktopBootstrapToken && cachedDefaultSessionToken) {
-      return cachedDefaultSessionToken;
+    const currentGeneration = serverBuildGeneration;
+    if (
+      credential === defaultDesktopBootstrapToken &&
+      cachedDefaultSessionToken &&
+      cachedDefaultSessionToken.generation === currentGeneration
+    ) {
+      return cachedDefaultSessionToken.token;
     }
 
     const { response, body } = yield* bootstrapBrowserSession(credential);
@@ -533,7 +539,7 @@
     }
 
     if (credential === defaultDesktopBootstrapToken) {
-      cachedDefaultSessionToken = body.sessionToken;
+      cachedDefaultSessionToken = { token: body.sessionToken, generation: currentGeneration };
     }
 
     return body.sessionToken;

diff --git a/apps/web/src/components/WebSocketConnectionSurface.tsx b/apps/web/src/components/WebSocketConnectionSurface.tsx
--- a/apps/web/src/components/WebSocketConnectionSurface.tsx
+++ b/apps/web/src/components/WebSocketConnectionSurface.tsx
@@ -54,10 +54,6 @@
   return "Retries exhausted trying to reconnect";
 }
 
-function buildReconnectTitle(_status: WsConnectionStatus): string {
-  return "Disconnected from T3 Server";
-}
-
 function describeRecoveredToast(
   previousDisconnectedAt: string | null,
   connectedAt: string | null,
@@ -270,7 +266,7 @@
                   ? `Reconnecting... ${formatReconnectAttemptLabel(status)}`
                   : `Reconnecting in ${formatRetryCountdown(status.nextRetryAt, nowMs)}... ${formatReconnectAttemptLabel(status)}`,
               timeout: 0,
-              title: buildReconnectTitle(status),
+              title: "Disconnected from T3 Server",
               type: "loading" as const,
               data: {
                 hideCopyButton: true,

You can send follow-ups to the cloud agent here.

@juliusmarminge juliusmarminge force-pushed the t3code/remote-auth-pairing branch from 3b06cc9 to 3759e05 Compare April 6, 2026 17:18
@juliusmarminge juliusmarminge force-pushed the t3code/remote-host-model branch from 314c455 to 54f905c Compare April 6, 2026 20:51
@juliusmarminge juliusmarminge force-pushed the t3code/remote-auth-pairing branch from 3759e05 to 4caff6f Compare April 7, 2026 04:30
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Desktop bootstrap token consumed but still advertised via IPC
    • The IPC handler now clears backendBootstrapToken after the first read, so subsequent calls return undefined instead of the stale, already-consumed token.

Create PR

Or push these changes by commenting:

@cursor push eebbb23e4b
Preview (eebbb23e4b)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -1239,10 +1239,14 @@
 
   ipcMain.removeAllListeners(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL);
   ipcMain.on(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL, (event) => {
+    const token = backendBootstrapToken || undefined;
+    if (token) {
+      backendBootstrapToken = "";
+    }
     event.returnValue = {
       label: "Local environment",
       wsUrl: backendWsUrl || null,
-      bootstrapToken: backendBootstrapToken || undefined,
+      bootstrapToken: token,
     } as const;
   });

You can send follow-ups to the cloud agent here.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Network-accessible preference permanently lost on transient network failure
    • Added a degraded flag to skip persisting the settings when the mode was downgraded from network-accessible to local-only due to no available LAN address, preserving the user's original preference.
  • ✅ Fixed: Loopback hostname check inconsistent with auth policy
    • Added normalizedHostname.startsWith("127.") to isLoopbackHostname in http.ts to align with the broader 127.x.x.x range already recognized by isLoopbackHost in ServerAuthPolicy.ts.

Create PR

Or push these changes by commenting:

@cursor push 3b7b4cb840
Preview (3b7b4cb840)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -220,11 +220,13 @@
     ...(advertisedHostOverride ? { advertisedHostOverride } : {}),
   });
 
+  let degraded = false;
   if (mode === "network-accessible" && exposure.mode !== "network-accessible") {
     if (options?.rejectIfUnavailable) {
       throw new Error("No reachable network address is available for this desktop right now.");
     }
     mode = "local-only";
+    degraded = true;
   }
 
   desktopServerExposureMode = exposure.mode;
@@ -241,7 +243,7 @@
   backendEndpointUrl = exposure.endpointUrl;
   backendAdvertisedHost = exposure.advertisedHost;
 
-  if (options?.persist || exposure.mode !== mode) {
+  if (!degraded && (options?.persist || exposure.mode !== mode)) {
     writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings);
   }
 

diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts
--- a/apps/server/src/http.ts
+++ b/apps/server/src/http.ts
@@ -34,7 +34,7 @@
     .trim()
     .toLowerCase()
     .replace(/^\[(.*)\]$/, "$1");
-  return LOOPBACK_HOSTNAMES.has(normalizedHostname);
+  return LOOPBACK_HOSTNAMES.has(normalizedHostname) || normalizedHostname.startsWith("127.");
 }
 
 export function resolveDevRedirectUrl(devUrl: URL, requestUrl: URL): string {

You can send follow-ups to the cloud agent here.

@juliusmarminge juliusmarminge force-pushed the t3code/remote-auth-pairing branch from 9f966e4 to dc69564 Compare April 7, 2026 23:31
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Unused authSessionRouteLayer export is dead code
    • Removed the unused authSessionRouteLayer export which was dead code, since only authSessionCorsRouteLayer (with CORS support) is actually imported and used in server.ts.

Create PR

Or push these changes by commenting:

@cursor push b263b7fc96
Preview (b263b7fc96)
diff --git a/apps/server/src/auth/http.ts b/apps/server/src/auth/http.ts
--- a/apps/server/src/auth/http.ts
+++ b/apps/server/src/auth/http.ts
@@ -28,17 +28,6 @@
     );
   });
 
-export const authSessionRouteLayer = HttpRouter.add(
-  "GET",
-  "/api/auth/session",
-  Effect.gen(function* () {
-    const request = yield* HttpServerRequest.HttpServerRequest;
-    const serverAuth = yield* ServerAuth;
-    const session = yield* serverAuth.getSessionState(request);
-    return HttpServerResponse.jsonUnsafe(session, { status: 200 });
-  }),
-);
-
 const REMOTE_AUTH_ALLOW_METHODS = "GET, POST, OPTIONS";
 const REMOTE_AUTH_ALLOW_HEADERS = "authorization, content-type";

You can send follow-ups to the cloud agent here.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Fallback log message never emitted due to mutated state
    • Captured the original serverExposureMode into a local variable before calling applyDesktopServerExposureMode, so the fallback check compares against the pre-mutation value.

Create PR

Or push these changes by commenting:

@cursor push 552d1cfcbe
Preview (552d1cfcbe)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -1667,18 +1667,16 @@
       `bootstrap restoring persisted server exposure mode mode=${desktopSettings.serverExposureMode}`,
     );
   }
-  const serverExposureState = await applyDesktopServerExposureMode(
-    desktopSettings.serverExposureMode,
-    {
-      persist: desktopSettings.serverExposureMode !== DEFAULT_DESKTOP_SETTINGS.serverExposureMode,
-    },
-  );
+  const requestedExposureMode = desktopSettings.serverExposureMode;
+  const serverExposureState = await applyDesktopServerExposureMode(requestedExposureMode, {
+    persist: requestedExposureMode !== DEFAULT_DESKTOP_SETTINGS.serverExposureMode,
+  });
   writeDesktopLogHeader(`bootstrap resolved backend endpoint baseUrl=${backendHttpUrl}`);
   if (serverExposureState.endpointUrl) {
     writeDesktopLogHeader(
       `bootstrap enabled network access endpointUrl=${serverExposureState.endpointUrl}`,
     );
-  } else if (desktopSettings.serverExposureMode === "network-accessible") {
+  } else if (requestedExposureMode === "network-accessible") {
     writeDesktopLogHeader(
       "bootstrap fell back to local-only because no advertised network host was available",
     );

You can send follow-ups to the cloud agent here.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Dev redirect skips non-loopback LAN requests to dev server
    • Removed the isLoopbackHostname guard from the dev redirect condition so all requests are redirected to the Vite dev server when devUrl is configured, regardless of the incoming hostname.

Create PR

Or push these changes by commenting:

@cursor push b05c9dd39a
Preview (b05c9dd39a)
diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts
--- a/apps/server/src/http.ts
+++ b/apps/server/src/http.ts
@@ -219,7 +219,7 @@
     }
 
     const config = yield* ServerConfig;
-    if (config.devUrl && isLoopbackHostname(url.value.hostname)) {
+    if (config.devUrl) {
       return HttpServerResponse.redirect(resolveDevRedirectUrl(config.devUrl, url.value), {
         status: 302,
       });

You can send follow-ups to the cloud agent here.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Loopback local URLs unreachable when binding specific host
    • Changed the specific-host branch in resolveDesktopServerExposure to compute localHttpUrl and localWsUrl using selectedHost instead of the hardcoded 127.0.0.1 loopback address, so the desktop app connects to the address the server actually binds to.

Create PR

Or push these changes by commenting:

@cursor push 94810f1dd5
Preview (94810f1dd5)
diff --git a/apps/desktop/src/serverExposure.ts b/apps/desktop/src/serverExposure.ts
--- a/apps/desktop/src/serverExposure.ts
+++ b/apps/desktop/src/serverExposure.ts
@@ -130,8 +130,8 @@
   return {
     mode: input.mode,
     bindHost: selectedHost,
-    localHttpUrl,
-    localWsUrl,
+    localHttpUrl: `http://${selectedHost}:${input.port}`,
+    localWsUrl: `ws://${selectedHost}:${input.port}`,
     endpointUrl: `http://${selectedHost}:${input.port}`,
     advertisedHost: selectedHost,
     availableHosts,

You can send follow-ups to the cloud agent here.

juliusmarminge and others added 16 commits April 8, 2026 19:07
Co-authored-by: Julius Marminge <julius@macmini.local>
Co-authored-by: Julius Marminge <julius@macmini.local>
Co-authored-by: Julius Marminge <julius@macmini.local>
Co-authored-by: Julius Marminge <julius@macmini.local>
- Persist and migrate the chosen exposure host
- Let desktop users pick a bind host before enabling network access
- Update IPC, server exposure resolution, and tests
- Assert the settings panel calls `setServerExposure` with mode and host
- Reflect the new network-accessible pairing flow in the browser test
- Centralize pairing link and session issuance/revocation
- Simplify desktop server exposure handling and IPC
- Update web settings and tests for the new auth flow
- Suppress minimum-log-level noise for pairing and session commands when emitting JSON
- Update CLI tests to capture stdout via TestConsole
- Keep composer assertions valid after thread canonicalization
- Wait for promoted drafts to settle before creating a fresh draft
- Replace bootstrap one-time credentials with 8-char pairing tokens
- Add coverage for the pairing token format
- Issue and consume pairing links via `#token=...`
- Keep query-token handling as a backward-compatible fallback
- Update CLI, server, and web tests for the new format
- Extend bootstrap one-time tokens from 8 to 12 chars
- Update the bootstrap credential test to match the new format
- Persist and surface auth session lastConnectedAt
- Replace qrcode.react with a vendored SVG QR renderer
- Update settings UI to show richer connection status
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge force-pushed the t3code/remote-auth-pairing branch from 8cef991 to aad7243 Compare April 9, 2026 02:09
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Expired seeded bootstrap credential returns wrong error message
    • Added a 'not_found' tag to ConsumeResult so expired seeded credentials return the specific 'Bootstrap credential expired.' error immediately instead of falling through to the database path which returns a generic 'Unknown bootstrap credential.' error.

Create PR

Or push these changes by commenting:

@cursor push 756ae604aa
Preview (756ae604aa)
diff --git a/apps/server/src/auth/Layers/BootstrapCredentialService.ts b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
--- a/apps/server/src/auth/Layers/BootstrapCredentialService.ts
+++ b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
@@ -19,6 +19,7 @@
 }
 
 type ConsumeResult =
+  | { readonly _tag: "not_found" }
   | {
       readonly _tag: "error";
       readonly error: BootstrapCredentialError;
@@ -175,13 +176,7 @@
         (current): readonly [ConsumeResult, Map<string, StoredBootstrapGrant>] => {
           const grant = current.get(credential);
           if (!grant) {
-            return [
-              {
-                _tag: "error",
-                error: invalidBootstrapCredentialError("Unknown bootstrap credential."),
-              },
-              current,
-            ];
+            return [{ _tag: "not_found" }, current];
           }
 
           const next = new Map(current);
@@ -227,6 +222,9 @@
       if (seededResult._tag === "success") {
         return seededResult.grant;
       }
+      if (seededResult._tag === "error") {
+        return yield* seededResult.error;
+      }
 
       const consumed = yield* pairingLinks.consumeAvailable({
         credential,

You can send follow-ups to the cloud agent here.

- Add per-request timeout and retry for backend readiness checks
- Fall back to local-only exposure when network access is unavailable
- Keep the existing window on app activate and remove react-scan script
- Create secrets with exclusive open and retry after AlreadyExists
- Treat expired bootstrap tokens as hard failures
- Improve CLI and keybinding validation messages
- Persist the user’s requested server exposure mode even when a safer mode is applied temporarily
- Route auth bootstrap cookies through the session credential service
- Tighten secret-store read error handling for concurrent startup
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Unused appliedMode parameter creates misleading API
    • Removed the unused appliedMode field from the function signature, its call site in main.ts, and the test.
  • ✅ Fixed: Session cookie name duplicated across independent modules
    • Extracted SESSION_COOKIE_NAME to Services/SessionCredentialService.ts and imported it in both ServerAuthPolicy.ts and the layer SessionCredentialService.ts, eliminating the duplicated constant.

Create PR

Or push these changes by commenting:

@cursor push d38c659f7a
Preview (d38c659f7a)
diff --git a/apps/desktop/src/desktopSettings.test.ts b/apps/desktop/src/desktopSettings.test.ts
--- a/apps/desktop/src/desktopSettings.test.ts
+++ b/apps/desktop/src/desktopSettings.test.ts
@@ -50,7 +50,6 @@
         },
         {
           requestedMode: "network-accessible",
-          appliedMode: "local-only",
         },
       ),
     ).toEqual({

diff --git a/apps/desktop/src/desktopSettings.ts b/apps/desktop/src/desktopSettings.ts
--- a/apps/desktop/src/desktopSettings.ts
+++ b/apps/desktop/src/desktopSettings.ts
@@ -14,7 +14,6 @@
   settings: DesktopSettings,
   input: {
     readonly requestedMode: DesktopServerExposureMode;
-    readonly appliedMode: DesktopServerExposureMode;
   },
 ): DesktopSettings {
   const persistedMode = input.requestedMode;

diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -237,7 +237,6 @@
   desktopServerExposureMode = exposure.mode;
   desktopSettings = setDesktopServerExposurePreference(desktopSettings, {
     requestedMode,
-    appliedMode: exposure.mode,
   });
   backendBindHost = exposure.bindHost;
   backendHttpUrl = exposure.localHttpUrl;

diff --git a/apps/server/src/auth/Layers/ServerAuthPolicy.ts b/apps/server/src/auth/Layers/ServerAuthPolicy.ts
--- a/apps/server/src/auth/Layers/ServerAuthPolicy.ts
+++ b/apps/server/src/auth/Layers/ServerAuthPolicy.ts
@@ -3,9 +3,8 @@
 
 import { ServerConfig } from "../../config.ts";
 import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
+import { SESSION_COOKIE_NAME } from "../Services/SessionCredentialService.ts";
 
-const SESSION_COOKIE_NAME = "t3_session";
-
 const isWildcardHost = (host: string | undefined): boolean =>
   host === "0.0.0.0" || host === "::" || host === "[::]";
 

diff --git a/apps/server/src/auth/Layers/SessionCredentialService.ts b/apps/server/src/auth/Layers/SessionCredentialService.ts
--- a/apps/server/src/auth/Layers/SessionCredentialService.ts
+++ b/apps/server/src/auth/Layers/SessionCredentialService.ts
@@ -6,6 +6,7 @@
 import { AuthSessionRepository } from "../../persistence/Services/AuthSessions.ts";
 import { ServerSecretStore } from "../Services/ServerSecretStore.ts";
 import {
+  SESSION_COOKIE_NAME,
   SessionCredentialError,
   SessionCredentialService,
   type IssuedSession,
@@ -21,7 +22,6 @@
 } from "../tokenCodec.ts";
 
 const SIGNING_SECRET_NAME = "server-signing-key";
-const SESSION_COOKIE_NAME = "t3_session";
 const DEFAULT_SESSION_TTL = Duration.days(30);
 const DEFAULT_WEBSOCKET_TOKEN_TTL = Duration.minutes(5);
 

diff --git a/apps/server/src/auth/Services/SessionCredentialService.ts b/apps/server/src/auth/Services/SessionCredentialService.ts
--- a/apps/server/src/auth/Services/SessionCredentialService.ts
+++ b/apps/server/src/auth/Services/SessionCredentialService.ts
@@ -81,6 +81,8 @@
   readonly markDisconnected: (sessionId: AuthSessionId) => Effect.Effect<void, never>;
 }
 
+export const SESSION_COOKIE_NAME = "t3_session";
+
 export class SessionCredentialService extends ServiceMap.Service<
   SessionCredentialService,
   SessionCredentialServiceShape

You can send follow-ups to the cloud agent here.

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Electron browser detection is unreachable dead code
    • Moved the Electron check before the Chrome check in inferBrowser so Electron's UA string (which contains 'Chrome/') is correctly identified as Electron.

Create PR

Or push these changes by commenting:

@cursor push bb1611aabd
Preview (bb1611aabd)
diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts
--- a/apps/server/src/auth/utils.ts
+++ b/apps/server/src/auth/utils.ts
@@ -67,10 +67,10 @@
   const normalized = userAgent;
   if (/Edg\//.test(normalized)) return "Edge";
   if (/OPR\//.test(normalized)) return "Opera";
+  if (/Electron\//.test(normalized)) return "Electron";
   if (/Firefox\//.test(normalized)) return "Firefox";
   if (/Chrome\//.test(normalized) || /CriOS\//.test(normalized)) return "Chrome";
   if (/Safari\//.test(normalized) && !/Chrome\//.test(normalized)) return "Safari";
-  if (/Electron\//.test(normalized)) return "Electron";
   return undefined;
 }

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 16a5be7. Configure here.

if (/Firefox\//.test(normalized)) return "Firefox";
if (/Chrome\//.test(normalized) || /CriOS\//.test(normalized)) return "Chrome";
if (/Safari\//.test(normalized) && !/Chrome\//.test(normalized)) return "Safari";
if (/Electron\//.test(normalized)) return "Electron";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Electron browser detection is unreachable dead code

Low Severity

inferBrowser checks for Chrome/ on line 71 before checking for Electron/ on line 73, but Electron's user-agent string always includes Chrome/. This means the Electron branch is unreachable dead code, and desktop Electron sessions will always be labeled as "Chrome" in AuthClientMetadata.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 16a5be7. Configure here.

- Stop retrying subscriptions after application-level stream failures
- Keep retry loops for transport disconnects only
- Add tests for both failure paths
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant